using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MyReactivity;
using MyReactivity.Abstractions;
using Project.Models.Abstractions;
using Project.Models.Enums;

namespace Project.Models
{
    public class Inventory : IModel, IReadOnlyInventory, IDisposable
    {
        private readonly ReactiveList<Slot> _slots;
        private readonly ReactiveList<IReadOnlySlot> _readOnlySlots;
        private readonly ReactiveProperty<bool> _isEmpty;

        protected readonly ItemConfigsStorage ItemConfigsStorage;

        public Inventory(
            int minQuantityOfSlots, 
            int maxQuantityOfSlots, 
            int quantityOfSlots, 
            ItemConfigsStorage itemConfigsStorage)
        {
            ItemConfigsStorage = itemConfigsStorage;
            MaxQuantityOfSlots = Math.Clamp(maxQuantityOfSlots, 0, int.MaxValue);
            MinQuantityOfSlots = Math.Clamp(minQuantityOfSlots, 0, MaxQuantityOfSlots);

            quantityOfSlots = Math.Clamp(quantityOfSlots, MinQuantityOfSlots, MaxQuantityOfSlots);

            _slots = new ReactiveList<Slot>(quantityOfSlots);
            _isEmpty = new ReactiveProperty<bool>(true);

            for (int i = 0; i < quantityOfSlots; i++)
                _slots.Add(new Slot(ItemConfigsStorage));

            _readOnlySlots = new ReactiveList<IReadOnlySlot>(_slots);

            _slots.ElementAdded += OnSlotsElementAdded;
            _slots.ElementRemoved += OnSlotsElementRemoved;
        }

        public readonly int MinQuantityOfSlots;
        public readonly int MaxQuantityOfSlots;

        public IReadOnlyReactiveList<Slot> Slots => _slots;
        public IReadOnlyReactiveProperty<bool> IsEmpty => _isEmpty;

        IReadOnlyReactiveList<IReadOnlySlot> IReadOnlyInventory.Slots => _readOnlySlots;

        public event Action<int, int> ItemsLost;

        public virtual void Dispose()
        {
            _slots.ElementAdded -= OnSlotsElementAdded;
            _slots.ElementRemoved -= OnSlotsElementRemoved;
        }

        public bool Has(int quantityOnItems) => GetTotalQuantityOfItems() >= quantityOnItems;
        public bool Has(int itemId, int quantityOnItems) => GetTotalQuantityOfItems(itemId) >= quantityOnItems;

        public int GetTotalQuantityOfItems() => _slots
            .Where(slot => slot.IsEmpty.Value)
            .Sum(slot => slot.QuantityOfItems.Value);
        public int GetTotalQuantityOfItems(int itemId) => _slots
            .Where(slot => slot.ItemId.Value == itemId)
            .Sum(slot => slot.QuantityOfItems.Value);

        public int AddItems(int itemId, int quantityOfItemsToAdd)
        {
            var remainingQuantityOfItemsToAdd = quantityOfItemsToAdd;

            for (int i = 0, length = _slots.Count; i < length; i++)
            {
                var slot = _slots[i];

                if (slot.ItemId.Value == itemId && !slot.IsFull.Value)
                {
                    remainingQuantityOfItemsToAdd = slot.Add(remainingQuantityOfItemsToAdd);

                    if (remainingQuantityOfItemsToAdd <= 0)
                        break;
                }
            }

            if (remainingQuantityOfItemsToAdd > 0)
                for (int i = 0, length = _slots.Count; i < length; i++)
                {
                    var slot = _slots[i];

                    if (slot.IsEmpty.Value)
                    {
                        remainingQuantityOfItemsToAdd = slot.Add(itemId, remainingQuantityOfItemsToAdd);

                        if (remainingQuantityOfItemsToAdd <= 0)
                            break;
                    }
                }

            if (remainingQuantityOfItemsToAdd > 0)
                ItemsLost?.Invoke(itemId, remainingQuantityOfItemsToAdd);

            RefreshIsEmpty();

            return remainingQuantityOfItemsToAdd;
        }

        public int RemoveItems(int itemId, int quantityOfItemsToRemove)
        {
            var remainingQuantityOfItemsToRemove = quantityOfItemsToRemove;

            for (int i = 0, length = _slots.Count; i < length; i++)
            {
                var slot = _slots[i];

                if (slot.ItemId.Value == itemId)
                {
                    remainingQuantityOfItemsToRemove = slot.Remove(remainingQuantityOfItemsToRemove);

                    if (remainingQuantityOfItemsToRemove <= 0)
                        break;
                }
            }

            RefreshIsEmpty();

            return remainingQuantityOfItemsToRemove;
        }

        public void AddSlotToEnd()
        {
            if (_slots.Count < MaxQuantityOfSlots)
                _slots.Add(new Slot(ItemConfigsStorage));
        }
        
        public void RemoveSlotFromEnd()
        {
            if (_slots.Count > MinQuantityOfSlots)
            {
                var index = _slots.Count - 1;
                var slot = _slots[index];

                if (!slot.IsEmpty.Value)
                    ItemsLost?.Invoke(slot.ItemId.Value, slot.QuantityOfItems.Value);

                _slots.RemoveAt(_slots.Count - 1);
                
                RefreshIsEmpty();
            }
        }

        public void SwapItems(int firstSlotIndex, int secondSlotIndex)
        {
            if (firstSlotIndex == secondSlotIndex)
                return;

            var firstSlot = _slots[firstSlotIndex];
            var secondSlot = _slots[secondSlotIndex];
            var tempSlot = secondSlot.Copy();

            secondSlot.Set(firstSlot);
            firstSlot.Set(tempSlot);
        }

        public void GroupItems()
        {
            var nonEmptySlots = _slots.Where(slot => !slot.IsEmpty.Value);
            var itemIds = nonEmptySlots.Select(slot => slot.ItemId.Value).ToHashSet();

            foreach (var itemId in itemIds)
            {
                var remainingQuantityOfItems = GetTotalQuantityOfItems(itemId);

                foreach (var slot in nonEmptySlots.Where(slot => slot.ItemId.Value == itemId))
                {
                    if (remainingQuantityOfItems <= 0)
                        slot.Clear();
                    else
                        remainingQuantityOfItems = slot.Set(remainingQuantityOfItems);
                }
            }
        }

        public void OrderItems(OrderItemsInInventoryBy by, bool isDescending = false)
        {
            var nonEmptySlots = _slots.Where(slot => !slot.IsEmpty.Value);
            IEnumerable<Slot> orderedSlots = null;

            switch (by)
            {
                case OrderItemsInInventoryBy.ItemId: 
                    orderedSlots = nonEmptySlots.OrderBy(slot => slot.ItemId.Value);
                    break;
                case OrderItemsInInventoryBy.QuantityOfItems:
                    var totalQuantitiesOfItems = nonEmptySlots
                        .Select(slot => slot.ItemId.Value)
                        .ToHashSet()
                        .ToDictionary(itemId => itemId, itemId => GetTotalQuantityOfItems(itemId));

                    orderedSlots = nonEmptySlots.OrderBy(slot => totalQuantitiesOfItems[slot.ItemId.Value]);
                    break;
                case OrderItemsInInventoryBy.ItemName:
                    orderedSlots = nonEmptySlots.OrderBy(slot => ItemConfigsStorage[slot.ItemId.Value].Name);
                    break;
            }

            if (isDescending)
                orderedSlots = orderedSlots.Reverse();

            for (int i = 0, length = orderedSlots.Count(); i < length; i++)
                SwapItems(_slots.IndexOf(orderedSlots.ElementAt(i)), i);
        }

        public void Clear()
        {
            foreach (var itemId in ItemConfigsStorage.ItemIds)
                Clear(itemId);
        }
        public void Clear(int itemId) => RemoveItems(itemId, GetTotalQuantityOfItems(itemId));

        public void Reset()
        {
            for (int i = 0, length = _slots.Count - MinQuantityOfSlots; i < length; i++)
                RemoveSlotFromEnd();

            Clear();
        }

        public override string ToString()
        {
            string getSlotText(int slotIndex)
            {
                var slot = _slots[slotIndex];

                return $"{slotIndex}:[{(slot.IsEmpty.Value ? "x" : $"{slot.ItemId.Value}|{slot.QuantityOfItems.Value}")}]";
            }

            var stringBuilder = new StringBuilder(getSlotText(0));

            for (int i = 1, length = _slots.Count; i < length; i++)
                stringBuilder.Append($" {getSlotText(i)}");

            return stringBuilder.ToString();
        }
    
        private void OnSlotsElementAdded(int index, Slot addedSlot) =>
            _readOnlySlots.Insert(index, addedSlot);
        private void OnSlotsElementRemoved(int index, Slot removedSlot) =>
            _readOnlySlots.RemoveAt(index);

        private void RefreshIsEmpty() => _isEmpty.Value = !_slots.Any(slot => !slot.IsEmpty.Value);
    }
}
